#include "connectionsmanager.h"
#include "bencodeparser.h"
#include "constants.h"
#include <QTimer>
#include <QCryptographicHash>
#include "filewriter.h"
#include "mainwindow.h"


/*
 * class responsible for managing multiple connections to torrent peers
 * associated with one .torrent file
 */
ConnectionsManager::ConnectionsManager(QObject* parent) :
        QObject(parent), totalBytesDownloaded(0),transferTimer(NULL),
        elapsedTime(NULL) ,fileWriter(NULL), downloadsMap(NULL)

{
    transferTimer = new QTimer(this);
    transferTimer->setInterval(10000);
    transferTimer->setSingleShot(false);
    elapsedTime = new QTime();
    downloadsMap = new QMap<TorrentConnection*, QPair<int, int> >();
    Q_ASSERT(connect(transferTimer, SIGNAL(timeout()), this, SLOT(updateTransferRate())));
}
ConnectionsManager::~ConnectionsManager()
{
    if(fileWriter)
        fileWriter->closeFile();
    delete downloadsMap;
}

void ConnectionsManager::peersDataReceived(QList<TorrentPeerData> &data)
{
    foreach(TorrentPeerData peerData, data)
    {
        TorrentConnection* conn = new TorrentConnection(peerData, *this, this);
        connectionsList.append(conn);
    }
    qDebug() << "ConnectionsManager:peersDataReceived: total peers count: ;"
            << QString::number(connectionsList.size());
}
void ConnectionsManager::startFile(MetaInfo& fileInfo, const QByteArray &infoHash, const QDir& path)
{
    fileMetaInfo = fileInfo;
    MetaInfoSingleFile singleFile = fileInfo.singleFile();
    fileWriter = new FileWriter(fileInfo, path, this);
    Q_ASSERT(connect(fileWriter, SIGNAL(error(const QString&)), this, SIGNAL(error(const QString&))));
    fileWriter->createAndOpenFile();
    Q_ASSERT(singleFile.pieceLength > 0);//TODO show error notification 'invalid piece length'
    int piecesCount =  (int) ( fileInfo.totalSize() / singleFile.pieceLength)
                       + ( (fileInfo.totalSize() % singleFile.pieceLength) ? 1: 0 ) ;
    filePieces.clear();
    int pieceLength = singleFile.pieceLength;
    for(int i = 0; i < piecesCount; i++)
    {        
        if(i == (piecesCount - 1) )
        {
            //last piece size is different
            int lastPieceSize = fileInfo.totalSize() % pieceLength;
            if(lastPieceSize) pieceLength = lastPieceSize;
        }
        TorrentFilePiece piece(pieceLength, BLOCK_SIZE_BYTES);
        filePieces.append(piece);
    }
    foreach (TorrentConnection* peerConnection, connectionsList)
    {
        peerConnection->init(infoHash);
    }

}
void ConnectionsManager::stopDownloadingFile()
{
    foreach(TorrentConnection* connection, connectionsList)
    {
        connection->stop();
    }
    fileWriter->closeFile();
    Q_ASSERT(disconnect(transferTimer, SIGNAL(timeout()), this, SLOT(updateTransferRate()) ));
}

void ConnectionsManager::requestNextDownload(TorrentConnection &peer)
{
    if(downloadsMap->contains(&peer))
    {
        downloadsMap->remove(&peer);
    }
    for(int pieceIndex = 0; pieceIndex < filePieces.count(); pieceIndex++)
    {
        if(peer.hasPiece(pieceIndex))
        {
            TorrentFilePiece& piece = filePieces[pieceIndex];
            TorrentFilePiece::State pieceState = piece.getState();
            if (TorrentFilePiece::DownloadFinished != pieceState )
            {
                int blockIndex = piece.downloadNextBlock();
                if (-1 == blockIndex)
                {
                    //means that the last block is being downloaded NOW
                    //download next piece
                    continue;
                }
                int begin = blockIndex * BLOCK_SIZE_BYTES;
                int length = piece.getBlockLength(blockIndex);
                qDebug() << "Downloading block" << blockIndex
                         << "of piece " << pieceIndex;

                peer.sendRequestPiece(pieceIndex, begin, length);
                qDebug() << "sending Request: piece "
                        << QString::number(pieceIndex)
                        << ", begin: "
                        << QString::number(begin)
                        <<", length: "
                        << QString::number(length);
                downloadsMap->insert(&peer, QPair<int, int>(pieceIndex,blockIndex));
                QString logStr = "Downloading piece " + QString::number(pieceIndex) + " ( begin "
                                 + QString::number(begin) + ", length: " +
                                 QString::number(length) + " from peer: "
                                 + peer.getPeerData().ipAddress.toString();
                GlobalLogger::getInstance()->log(logStr);
                emit downloadsTableChanged(downloadsMap);
                return;
            }
        }
    }
    //if reached to that point, we are in the end game
    endGameStuff();
}

void ConnectionsManager::endGameStuff()
{
    qDebug() << "END GAME STUFF";
    //TODO end game; download one of the pieces already being downloaded, but slowly
    TorrentConnection* idlePeer = getIdlePeer();
    if(NULL == idlePeer)
    {
        qDebug() << "Idle peer not found!";
        return;
    }
    for(int pieceIndex = 0; pieceIndex < filePieces.count(); pieceIndex++)
    {
        TorrentFilePiece& piece = filePieces[pieceIndex];
        TorrentFilePiece::State pieceState = piece.getState();
        if(TorrentFilePiece::DownloadingInProgress == pieceState)
        {
            int blockIndex = piece.getFirstDownloadingBlock();
            if(-1 != blockIndex)
            {
                requestDownload(*idlePeer, pieceIndex, blockIndex);
                return;
            }
        }
    }
}
void ConnectionsManager::requestDownload(TorrentConnection &peer, int pieceIndex, int blockIndex)
{
    TorrentFilePiece& piece = filePieces[pieceIndex];
    int begin = blockIndex * BLOCK_SIZE_BYTES;
    int length = piece.getBlockLength(blockIndex);
    qDebug() << "Downloading block" << blockIndex
             << "of piece " << pieceIndex;

    peer.sendRequestPiece(pieceIndex, begin, length);
    qDebug() << "sending Request: piece "
            << QString::number(pieceIndex)
            << ", begin: "
            << QString::number(begin)
            <<", length: "
            << QString::number(length);
    downloadsMap->insert(&peer, QPair<int, int>(pieceIndex,blockIndex));
    QString logStr = "Downloading piece " + QString::number(pieceIndex) + " ( begin "
                     + QString::number(begin) + ", length: " +
                     QString::number(length) + " from peer: "
                     + peer.getPeerData().ipAddress.toString();
    GlobalLogger::getInstance()->log(logStr);
    emit downloadsTableChanged(downloadsMap);
}

TorrentConnection* ConnectionsManager::getIdlePeer()
{
    foreach(TorrentConnection* peer, connectionsList)
    {
        if(!peer->getPeerData().peerChoking)
        {
            if(!downloadsMap->contains(peer))
            {
                qDebug() << "ConnectionsManager::getIdlePeer: found "
                        + peer->getPeerData().ipAddress.toString();
                return peer;
            }
        }
    }
    qDebug() << "ConnectionsManager::getIdlePeer: not found";
    return NULL;
}

void ConnectionsManager::chokeReceived(TorrentConnection& peer)
{
    if(downloadsMap->contains(&peer))
    {
        QPair<int, int> blockPiecePair = downloadsMap->value(&peer);
        int pieceIndex = blockPiecePair.first;
        int blockIndex = blockPiecePair.second;
        Q_ASSERT(pieceIndex < filePieces.size() );
        qDebug() << "ConnectionsManager:chokeReceived: stopped downloading piece"
                << pieceIndex <<", block " << blockIndex;

        filePieces[pieceIndex].setBlockNotDownloading(blockIndex);
        TorrentConnection* idlePeer = getIdlePeer();
        if(idlePeer)
        {
            requestNextDownload(*idlePeer);
        }
    }
}
void ConnectionsManager::unchokeReceived(TorrentConnection& peer)
{
    qDebug() << "\nConnetionsManager::unchokeReceived:";
    GlobalLogger::getInstance()->log("Received UNCHOKE from peer " +
                                     peer.getPeerData().ipAddress.toString());
    requestNextDownload(peer);
    if(!transferTimer->isActive())
    {
        transferTimer->start();
        elapsedTime->start();
    }
}
void ConnectionsManager::pieceReceived(TorrentConnection &peer, const QByteArray& piece)
{
    qDebug() << "ConnectionsManager::pieceReceived()";    
    QByteArray pieceIndexBytes = piece.mid(0, 4);
    QByteArray blockBeginBytes = piece.mid(4, 4) ;
    QByteArray data = piece.mid(8);
    quint32 pieceIndex = TorrentConnection::convertFromBigEndian( pieceIndexBytes );
    quint32 blockBegin = TorrentConnection::convertFromBigEndian( blockBeginBytes);
    qDebug() << "piece index: " << pieceIndex <<", offset: " << blockBegin;
    if(pieceIndex >= (quint32) filePieces.size())
    {
        qDebug() << "invalid piece index?";
        return;
    }
    int blockIndex = blockBegin / BLOCK_SIZE_BYTES;
    filePieces[pieceIndex].setBlockDownloaded(blockIndex);
    int size = piece.size() - 8;
    totalBytesDownloaded += size;
    qDebug() << "total bytes downloaded so far: " << totalBytesDownloaded;
    //SAVE TO FILE
    fileWriter->writeToFile(pieceIndex * fileMetaInfo.singleFile().pieceLength + blockBegin,
                          data);

    downloadsMap->remove(&peer);
    //cancel all pending downloads of this piece if any
    foreach(TorrentConnection* peer, downloadsMap->keys())
    {
        QPair<int, int> value = downloadsMap->value(peer, QPair<int,int>(-1, -1));
        if((qint32) pieceIndex == value.first && blockIndex == value.second)
        {
            int length = filePieces[pieceIndex].getBlockLength(blockIndex);
            peer->cancelPiece(pieceIndex, blockBegin, length);
        }
    }

    if(checkAllPiecesDownloaded())
    {
         //TODO check data validity - pieces' hashes
        if (checkDataValidity())
        {
            qDebug() <<"===============";
            qDebug() << "FILE DOWNLOADED!";
            qDebug() << "===============";
            fileWriter->closeFile();
            fileWriter->setFileDownloaded(true);
            stopDownloadingFile();
            emit downloadCompleted();
            return;
        }
    }
    requestNextDownload(peer);
}

void ConnectionsManager::updateTransferRate()
{
    if(!elapsedTime->elapsed())
        return;
    int elapsedTimeMsec = elapsedTime->elapsed();
    double totalKbytesDownloaded = (double) totalBytesDownloaded / 1024.0;
    double secondsElapsed = (double) elapsedTimeMsec / 1000 ;
    transferRate =   (double) totalBytesDownloaded / elapsedTimeMsec  ;
    qDebug() <<"transfer rate: downloaded " << totalKbytesDownloaded
            <<"kB in" << secondsElapsed<<" seconds, transfer: "
            << transferRate << "kB/s";
    emit transferRateUpdated(transferRate);
}
bool ConnectionsManager::checkAllPiecesDownloaded()
{
    foreach(const TorrentFilePiece& piece, filePieces)
    {
        if(TorrentFilePiece::DownloadFinished != piece.getState())
        {
            return false;
        }
    }
    return true;
}
bool ConnectionsManager::checkDataValidity()
{
    qDebug() << "ConnectionsManager::checkDataValidity()" ;
    bool dataValid = true;
    for(int i = 0; i < filePieces.size(); i++)
    {
       TorrentFilePiece& piece = filePieces[i];
       QByteArray pieceHash = fileMetaInfo.singleFile().sha1Sums.at(i);
       int size = fileMetaInfo.singleFile().pieceLength;
       int offset = i * size;
       if( (filePieces.size() - 1) == i )
       {
           //last piece's size is different
           size = fileMetaInfo.totalSize() % size;
       }
       const QByteArray& pieceData = fileWriter->readFromFile(offset, size);
       if(!&pieceData)  //check if not null
       {           
           piece.setInvalid();
           return false;
       }
       QByteArray hashCalculated = QCryptographicHash::hash(pieceData,
                                                            QCryptographicHash::Sha1);
       qDebug() << "checking piece " << i;
       qDebug() << "hash from .torrent file: " << pieceHash;
       qDebug() << "calculated hash: " << hashCalculated;
       Q_ASSERT(pieceHash.size() == hashCalculated.size());
       bool result = (pieceHash == hashCalculated);
       qDebug() <<"comparision result: " << result;
       if(!result)
       {
           piece.setInvalid();
           dataValid = false;
       }
    }
    return dataValid;
}

TorrentFilePiece::TorrentFilePiece(int _pieceLength, int _blockSize) :
        pieceLength(_pieceLength), blockSize(_blockSize), pieceState(NotDownloading)

{
    Q_ASSERT(blockSize);
    blocksCount = pieceLength / blockSize + ((pieceLength % blockSize) ? 1 : 0);
    blocks = QList<State>();
    for(int i = 0; i < blocksCount; i++)
    {
        blocks.append(NotDownloading);
    }
}
int TorrentFilePiece::downloadNextBlock()
{
    qDebug() <<"\nTorrentFilePiece::downloadNextBlock";
    for (int i = 0; i < blocks.size(); i++)
    {
        if(NotDownloading == blocks.at(i))
        {
            blocks.replace(i, DownloadingInProgress);
            qDebug() << "downloading block " <<i  << "\n";
            if(NotDownloading == pieceState)
            {
                pieceState = DownloadingInProgress;
            }
            return i;
        }
    }
    qDebug() << "all blocks are being downloaded!";
    return -1;
}
void TorrentFilePiece::setBlockDownloaded(int blockIndex)
{
    blocks.replace(blockIndex, DownloadFinished);
    //check if all blocks have been downloaded
    foreach(State state, blocks)
    {
        if(DownloadFinished != state)
            return;
    }
    pieceState = DownloadFinished;

}
void TorrentFilePiece::setBlockNotDownloading(int blockIndex)
{
    blocks.replace(blockIndex, NotDownloading);
    foreach(State state, blocks)
    {
        if(NotDownloading != state)
            return;
    }
    //all blocks within the piece are not being downloaded - change piece state
    pieceState = NotDownloading;

}
void TorrentFilePiece::setInvalid()
{
    pieceState = NotDownloading;
    for(int i =0; i < blocks.size(); i++)
    {
      blocks[i] = NotDownloading;
    }
}

TorrentFilePiece::State TorrentFilePiece::getState() const
{
    return pieceState;
}
int TorrentFilePiece::getBlockLength(int blockIndex)
{
    if(blockIndex != blocksCount - 1)
    {
        return blockSize;
    }
    else
    {
        int returned  = pieceLength % blockSize;
        return (returned) ? returned  : blockSize;
    }
}

int TorrentFilePiece::getFirstDownloadingBlock()
{
    for (int i=0; i < blocks.size(); i++)
    {
        Q_ASSERT(NotDownloading != blocks[i]);
        if(DownloadingInProgress == blocks[i])
            return i;
    }
    return -1;
}
